package com.daqian.chance.example.admin.config;

import cn.hutool.core.util.StrUtil;
import com.daqian.chance.core.util.MD5Util;
import com.daqian.chance.example.admin.core.filter.ShiroPermissionsFilter;
import com.daqian.chance.example.admin.modules.system.po.entity.SysMenu;
import com.daqian.chance.example.admin.modules.system.po.entity.SysRole;
import com.daqian.chance.example.admin.modules.system.po.entity.SysUser;
import com.daqian.chance.example.admin.modules.system.service.ISysMenuService;
import com.daqian.chance.example.admin.modules.system.service.ISysRoleService;
import com.daqian.chance.example.admin.modules.system.service.ISysUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;
import javax.servlet.Filter;
import java.util.*;

/**
 * Shiro配置类
 *
 * @author: daqian
 * @date: 2019/8/6 16:43
 */
@Configuration
@Slf4j
public class ShiroConfig {
    @Value("${spring.redis.host}")
    private String redisHost;
    @Value("${spring.redis.port}")
    private int redisPort;
    @Value("${spring.redis.timeout}")
    private int redisTimeout;
    @Value("${spring.redis.password}")
    private String redisPassword;
    @Value("${spring.redis.database}")
    private int redisDatabase;

    /**
     * ShiroFilterFactoryBean 处理拦截资源文件问题。
     * 注意：单独一个ShiroFilterFactoryBean配置是或报错的，以为在
     * 初始化ShiroFilterFactoryBean的时候需要注入：SecurityManager
     * <p>
     * <p>
     * Filter Chain定义说明 1、一个URL可以配置多个Filter，使用逗号分隔 2、当设置多个过滤器时，全部验证通过，才视为通过
     * 3、部分过滤器可指定参数，如perms，roles
     */

    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        // 1.必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 2.如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        //shiroFilterFactoryBean.setLoginUrl("/login.ftl");

        // 3.配置登录页面 todo xxx
//        shiroFilterFactoryBean.setLoginUrl("/static/login");
//        shiroFilterFactoryBean.setUnauthorizedUrl("/sys/logout");

        // 4.权限过滤器配置
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 4.1.放行登录接口
        filterChainDefinitionMap.put("/sys/login", "anon");
        filterChainDefinitionMap.put("/sys/logout", "anon");

        // 4.2.放行swagger2接口
        // http://{IP:PORT}/{rootPath}/swagger-ui.html
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon");
        filterChainDefinitionMap.put("/v2/api-docs/**", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        // http://{IP:PORT}/{rootPath}/doc.html
        filterChainDefinitionMap.put("/doc.html", "anon");

        // 4.3.放行static资源
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/error/**", "anon");

        // 4.4.放行druid接口
        // http://{IP:PORT}/{rootPath}/druid/sql.html
        filterChainDefinitionMap.put("/druid/**", "anon");

        // 4.5.配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
//        filterChainDefinitionMap.put("/sys/logout", "logout");
        // todo xxx 测试用：放行 test 接口
        filterChainDefinitionMap.put("/test/**", "anon");

        // 过滤链定义，从上向下顺序执行，一般将 /**放在最为下边
        // authc:所有url都必须认证通过才可以访问;
        // anon:所有url都都可以匿名访问
        filterChainDefinitionMap.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);


        // 自定义过滤器
        Map<String, Filter> filtersMap = new LinkedHashMap<>();
        filtersMap.put("perms", new ShiroPermissionsFilter());
        shiroFilterFactoryBean.setFilters(filtersMap);

        return shiroFilterFactoryBean;
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(cacheManager());
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        // 设置realm
        securityManager.setRealm(myRealm());
        return securityManager;
    }

    /**
     * 身份认证realm; (这个需要自己写，账号密码校验；权限等)
     *
     * @return
     */
    @Bean
    public MyRealm myRealm() {
        MyRealm myRealm = new MyRealm();
        return myRealm;
    }

    /**
     * Shiro生命周期处理器
     * 加static原因：加了static能解决@value值获取不到到问题；可能是此配置文件在读取yml之前加载了
     * 参考：https://blog.csdn.net/wuxuyang_7788/article/details/70141812
     *
     * @return
     */
    @Bean
    public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;
     * <p>
     * 使授权注解起作用不如不想配置可以在pom文件中加入
     * <dependency>
     * <groupId>org.springframework.boot</groupId>
     * <artifactId>spring-boot-starter-aop</artifactId>
     * </dependency>
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * cacheManager 缓存 redis实现
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        //redis中针对不同用户缓存
        redisCacheManager.setKeyPrefix("username");
        return redisCacheManager;
    }

    /**
     * 配置shiro redisManager
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(redisHost + ":" + redisPort);
//        redisManager.setPort(redisPort);
        redisManager.setDatabase(redisDatabase);
        redisManager.setPassword(StrUtil.isEmpty(redisPassword) ? null : redisPassword);
        // 配置缓存过期时间
        redisManager.setTimeout(redisTimeout);
        redisManager.setDatabase(redisDatabase);
        return redisManager;
    }

    /**
     * Session Manager
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }

    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

    /**
     * 自定义Realm
     *
     * @author zjt
     */
    public class MyRealm extends AuthorizingRealm {

        @Resource
        private ISysRoleService iSysRoleService;

        @Resource
        private ISysMenuService iSysMenuService;

        @Autowired
        private ISysUserService iSysUserService;


        /**
         * 身份认证。即登录通过账号和密码验证登录人的身份信息
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            String userName = (String) authenticationToken.getPrincipal();
            // 根据用户名查询出用户记录
            SysUser sysUser = iSysUserService.selectUserByUserName(userName);
            if (sysUser == null) {
                throw new AccountException("用户不存在");
            }
            log.debug("sysUser是：{}", sysUser);
            String userPwd = new String((char[]) authenticationToken.getCredentials());
            log.debug("密码是：{}", userPwd);
            if (!MD5Util.encrypt(userPwd).equals(sysUser.getPassword())) {
                throw new AccountException("密码错误");
            }
            AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(sysUser.getUsername(), userPwd, sysUser.getUsername());
            return authcInfo;
        }

        /**
         * 权限认证(验证权限时调用)
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            String userName = (String) SecurityUtils.getSubject().getPrincipal();
            // 根据用户名查询出用户记录
            SysUser sysUser = iSysUserService.selectUserByUserName(userName);
            if (sysUser == null) {
                throw new AccountException("用户不存在");
            }
            // 根据用户id查询用户角色
            List<SysRole> roleList = iSysRoleService.selectRolesByUserId(sysUser.getId());

            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            Set<String> roles = new HashSet<>();
            info.setRoles(roles);
            if (roleList == null || roleList.size() == 0) {
                return info;
            }
            for (SysRole role : roleList) {
                roles.add(role.getRoleName());
                // 根据角色id查询所有资源
                List<SysMenu> menuList = iSysMenuService.selectMenusByRoleId(role.getId());
                if (menuList == null || menuList.size() == 0) {
                    continue;
                }
                for (SysMenu menu : menuList) {
                    // 添加权限
                    info.addStringPermission(menu.getMenuCode());
                }
            }
            info.setRoles(roles);
            return info;
        }
    }
}